Skip to content

Conversation

@Gyuhyeok99
Copy link
Contributor

관련 이슈

작업 내용

채팅방 조회 n + 1 문제를 개선하였습니다.

AS-IS

  • 1 + 3N 쿼리 발생
  • 사용자 조회 (1개)
  • 채팅방 조회 (1개)
  • 각 채팅방별 반복 (3N개) -> 최신 메시지 조회(N), 사용자 조회(N), 안읽은 메시지 수 조회(N)
  • 참가자 배치 조회(1개) - @batchsize(10)

TO-BE

  • 5개 쿼리 발생(고정)
  • 사용자 조회 (1개) - 동일
  • 채팅방 + 참가자 한번에 조회 (1개)
  • 최신 메시지들 배치 조회 (1개)
  • 안읽은 메시지 수들 배치 조회 (1개)
  • 파트너 사용자들 배치 조회 (1개)

해결 방법

  1. JOIN FETCH 활용

기존: 채팅방 조회 → 참가자 개별 조회 (N+1)
개선: 채팅방 + 참가자 한번에 조회

  1. 배치 활용:

기존: 각 채팅방별로 개별 쿼리 (× 3)
개선: WHERE ... IN (chatRoomIds) 로 배치 처리

  1. 메모리 + Map 활용:

DB에서 가져온 데이터를 Map으로 변환 후 메모리에서 조합
추가 DB 접근 없이 Response 생성

채팅방 만약 20개였다면 62개(1 + 1 + 3N + 2)가 나가던 쿼리가 5개로 줄어들었습니다.

특이 사항

리뷰 요구사항 (선택)

@coderabbitai
Copy link

coderabbitai bot commented Aug 24, 2025

Walkthrough

    1. 새 DTO(UnreadCountDto)와 집계 레코드(ChatRoomData)가 추가되었습니다.
    1. ChatMessageRepository에 채팅방별 최신 메시지 일괄 조회와 사용자 기준 미읽음 카운트 일괄 조회 JPQL 메서드가 도입되었습니다.
    1. ChatRoomRepository의 1:1 채팅방 조회가 참가자 fetch-join과 정렬을 포함하는 메서드로 대체되었고, 개별 미읽음 카운트 메서드는 제거되었습니다.
    1. ChatService는 배치 기반 조회로 리팩터링되었고, ChatRoomData를 활용해 최신 메시지/미읽음/상대 사용자 정보를 한 번에 구성합니다.
    1. SiteUserRepository에 ID 목록 기반 배치 조회 메서드가 추가되었습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • wibaek
  • whqtker
  • lsy1307
  • nayonsoso

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (9)
src/main/java/com/example/solidconnection/chat/dto/UnreadCountDto.java (1)

3-8: DTO 투영 안정성·가독성 개선 제안(Long 래핑, 네이밍 명확화).

    1. 호환성. JPQL COUNT는 Long을 반환하므로, 레코드 생성자 파라미터를 primitive long 대신 Long으로 두면 프로바이더별 박싱/언박싱 이슈를 예방할 수 있습니다.
    1. 가독성. 필드명을 count → unreadCount로 바꾸면 도메인 의미가 즉시 드러납니다.
    1. 변경 영향. 조회 전용 DTO라 사이드 이펙트는 없고, 호출부 리네임만 필요합니다.

아래 diff 적용 후, 컴파일 에러가 없는지와 JPQL 생성자 표현식이 정상 바인딩되는지 확인 부탁드립니다.

-public record UnreadCountDto(
-        long chatRoomId,
-        long count
-) {
+public record UnreadCountDto(
+        Long chatRoomId,
+        Long unreadCount
+) {
src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java (1)

23-23: 대량 조회 파라미터를 Collection으로 일반화하고 빈 리스트 처리 컨벤션 합의.

    1. 유연성. List → Collection으로 넓히면 Set 등 다양한 컨테이너를 바로 받을 수 있어 중복 제거·메모리 효율에 유리합니다.
    1. 안정성. 파라미터가 비었을 때 서비스 레이어에서 즉시 빈 결과를 반환하는 가드(early-return) 컨벤션을 두면 불필요한 DB round-trip을 피할 수 있습니다.
    1. 성능. IN 절 파라미터가 매우 클 수 있으니(수천 개 이상) 호출부에서 적절한 청크 분할도 고려해 주세요.

아래 시그니처 변경 후, 호출부 타입 불일치가 없는지 점검해 주세요.

-    List<SiteUser> findAllByIdIn(List<Long> ids);
+    List<SiteUser> findAllByIdIn(java.util.Collection<Long> ids);

추가 import가 필요하다면 다음 한 줄을 상단 import 블록에 포함하세요.

import java.util.Collection;
src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java (2)

33-50: 인덱스 제안으로 배치 쿼리 체감 성능 확보.

    1. ChatMessage(chat_room_id, created_at) 복합 인덱스: 최신 메시지 선정·미읽음 비교 모두 가속.
    1. ChatReadStatus(chat_room_id, chat_participant_id): 조인 키 정합성·룩업 최적화.
    1. ChatParticipant(chat_room_id, site_user_id): 사용자-채팅방 역방향 탐색 최적화.

원하시면 DDL 스니펫을 엔티티 @Index 또는 마이그레이션 스크립트 형태로 드래프트해 드리겠습니다.


33-50: 미읽음 배치 카운트 쿼리 성능·가독성 개선 제안

  1. 조인 경로 재구성
      - cm.chatRoom.chatParticipants를 JOIN하여 correlated 서브쿼리(SELECT cp.id …) 제거
  2. COUNT DISTINCT 적용
      - COUNT(cm) 대신 COUNT(DISTINCT cm.id) 사용으로 조인 폭발 시 중복 방지
  3. 가독성 강화
      - 연관 엔티티 경로 조인으로 의도를 명확히 표현 (JOIN cm.chatRoom.chatParticipants cp)
  4. 빈 chatRoomIds 처리
      - chatRoomIds가 빈 경우, 호출부에서 즉시 빈 결과를 반환하는 가드 추가 권장

실제 엔티티 필드명(chatParticipants, chatRoomId, chatParticipantId 등)이 다를 수 있으니 매핑을 확인하고 적절히 치환해 주세요.

-    @Query("""
-           SELECT new com.example.solidconnection.chat.dto.UnreadCountDto(
-               cm.chatRoom.id,
-               COUNT(cm)
-           )
-           FROM ChatMessage cm
-           LEFT JOIN ChatReadStatus crs ON crs.chatRoomId = cm.chatRoom.id
-               AND crs.chatParticipantId = (
-                   SELECT cp.id FROM ChatParticipant cp
-                   WHERE cp.chatRoom.id = cm.chatRoom.id
-                   AND cp.siteUserId = :userId
-               )
-           WHERE cm.chatRoom.id IN :chatRoomIds
-           AND cm.senderId != :userId
-           AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt)
-           GROUP BY cm.chatRoom.id
-           """)
+    @Query("""
+           SELECT new com.example.solidconnection.chat.dto.UnreadCountDto(
+               cm.chatRoom.id,
+               COUNT(DISTINCT cm.id)
+           )
+           FROM ChatMessage cm
+           JOIN cm.chatRoom.chatParticipants cp
+           LEFT JOIN ChatReadStatus crs
+             ON crs.chatRoomId = cm.chatRoom.id
+            AND crs.chatParticipantId = cp.id
+           WHERE cp.siteUserId = :userId
+             AND cm.chatRoom.id IN :chatRoomIds
+             AND cm.senderId <> :userId
+             AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt)
+           GROUP BY cm.chatRoom.id
+           """)
src/main/java/com/example/solidconnection/chat/dto/ChatRoomData.java (1)

9-24: toMap 중복 키 예외 방지와 불변 Map 래핑으로 안전성·예측가능성 향상.

    1. 중복 키 안전. 실무에서 중복이 거의 없더라도 toMap 기본 동작은 중복 시 IllegalStateException을 던집니다. 머지 전략을 명시해 방어적으로 갑시다.
    1. 결정 규칙. latestMessages는 createdAt(동률 시 id) 기준 최댓값을 선택, unreadCounts는 동일 키 중복 시 최댓값 유지, partnerUsers는 최초 항목 유지가 합리적입니다.
    1. 불변성. Map.copyOf로 결과를 불변화하면 이후 파이프라인에서 실수로 변형되는 것을 막습니다.
    1. 제너럴리티. 파라미터 타입을 List → Collection으로 넓히면 입력 유연성이 올라갑니다.

아래 diff는 충돌 방지를 위해 핵심 라인만 제시합니다.

-    public static ChatRoomData from(List<ChatMessage> latestMessages,
-                                    List<UnreadCountDto> unreadCounts,
-                                    List<SiteUser> partnerUsers) {
+    public static ChatRoomData from(
+            java.util.Collection<ChatMessage> latestMessages,
+            java.util.Collection<UnreadCountDto> unreadCounts,
+            java.util.Collection<SiteUser> partnerUsers) {
         return new ChatRoomData(
-                latestMessages.stream().collect(Collectors.toMap(msg -> msg.getChatRoom().getId(), msg -> msg)),
-                unreadCounts.stream().collect(Collectors.toMap(UnreadCountDto::chatRoomId, UnreadCountDto::count)),
-                partnerUsers.stream().collect(Collectors.toMap(SiteUser::getId, user -> user))
+                java.util.Map.copyOf(
+                    latestMessages.stream().collect(Collectors.toMap(
+                        msg -> msg.getChatRoom().getId(),
+                        msg -> msg,
+                        (a, b) -> {
+                            var aTs = a.getCreatedAt();
+                            var bTs = b.getCreatedAt();
+                            if (aTs.equals(bTs)) {
+                                return a.getId() >= b.getId() ? a : b; // 동률 시 더 큰 id
+                            }
+                            return aTs.isAfter(bTs) ? a : b;         // 최신 시각 우선
+                        }
+                    ))
+                ),
+                java.util.Map.copyOf(
+                    unreadCounts.stream().collect(Collectors.toMap(
+                        UnreadCountDto::chatRoomId,
+                        UnreadCountDto::unreadCount,
+                        java.lang.Long::max
+                    ))
+                ),
+                java.util.Map.copyOf(
+                    partnerUsers.stream().collect(Collectors.toMap(
+                        SiteUser::getId,
+                        java.util.function.Function.identity(),
+                        (a, b) -> a
+                    ))
+                )
         );
     }
src/main/java/com/example/solidconnection/chat/service/ChatService.java (4)

71-76: 목록 정렬 기준을 ‘최근 메시지’로 고정할지 확인하고, 필요 시 서비스에서 정렬을 적용해 주세요.
현재는 리포지토리 반환 순서를 그대로 사용합니다. 제품 기대가 “최근 대화순”이라면 최신 메시지 시각 기준 내림차순 정렬을 서비스에서 보장하는 편이 안전합니다.
1) 리포지토리 레벨에서 이미 정렬을 보장한다면 유지해도 됩니다.
2) 정렬 보장이 명확하지 않다면 아래 정렬을 권장합니다. null 최신 메시지는 뒤로 밀리고, 최신순으로 정렬됩니다.

정렬 추가 예시(최소 변경).

-        List<ChatRoomResponse> responses = chatRooms.stream()
-                .map(chatRoom -> createChatRoomResponse(chatRoom, siteUserId, chatRoomData))
-                .toList();
+        List<ChatRoomResponse> responses = chatRooms.stream()
+                .sorted(java.util.Comparator.comparing(
+                        (ChatRoom room) -> {
+                            ChatMessage lm = chatRoomData.latestMessages().get(room.getId());
+                            return lm != null ? lm.getCreatedAt() : null;
+                        },
+                        java.util.Comparator.nullsLast(java.util.Comparator.naturalOrder())
+                ).reversed())
+                .map(chatRoom -> createChatRoomResponse(chatRoom, siteUserId, chatRoomData))
+                .toList();

참고: 위 코드는 java.util.Comparator 정규 참조를 사용해 별도 import 없이 동작합니다.


61-76: 행위 변화 회귀 테스트를 보강해 주세요.
배치 로딩으로 전환되며 정렬, 중복 제거, 비어있는 최신 메시지 케이스 등 경계조건을 검증하는 테스트가 중요합니다.
1) 채팅방 2개 이상에서 최신 메시지 시각 기준 정렬이 기대대로인지 검증해 주세요.
2) fetch-join으로 인한 중복 결과가 응답에 나타나지 않는지 검증해 주세요.
3) 최신 메시지가 존재하지 않는 방에 대한 폴백(content="", createdAt=null) 계약을 검증해 주세요.
4) 미읽음 카운트가 없는 방의 기본값이 0으로 세팅되는지 검증해 주세요.
5) 파트너 사용자가 비정상/삭제 상태일 때의 처리 정책을 검증해 주세요.


78-89: 파트너 사용자 배치 조회에서 중복 ID를 제거해 IN 절을 최적화해 주세요.
같은 상대와 여러 방이 없더라도, 스트림 연산상 중복 ID가 섞일 가능성은 존재합니다. 불필요한 IN 항목을 제거하면 DB 파서 부담을 줄일 수 있습니다.
또한, 본 블록에서 findPartner가 예외를 던지면 전체 목록 구성이 중단됩니다. 리포지토리 단계가 1:1 방만 보장하는지 함께 확인해 주세요.
1) ID 중복 제거를 권장합니다.
2) 그룹방/비정상 데이터가 섞일 경우 전체 실패가 발생하므로, 리포지토리에서 철저히 1:1 방만 반환되도록 보장하거나, 서비스에서 예외 전파 정책을 명확히 해 주세요.
3) 성능과 가독성을 위해 findPartner 결과를 미리 roomId->partnerUserId 맵으로 캐싱해 두고 재사용하는 구조도 고려할 수 있습니다.

중복 제거 최소 변경 예시.

-        List<Long> partnerUserIds = chatRooms.stream()
-                .map(chatRoom -> findPartner(chatRoom, siteUserId).getSiteUserId())
-                .toList();
+        List<Long> partnerUserIds = chatRooms.stream()
+                .map(chatRoom -> findPartner(chatRoom, siteUserId).getSiteUserId())
+                .distinct()
+                .toList();

91-107: 파트너 사용자 누락 시 전체 실패 처리 정책을 재확인해 주세요.
현재는 partnerUser가 null이면 USER_NOT_FOUND 예외를 던져 전체 리스트 구성을 중단합니다. 운영 관점에서 “문제 방만 건너뛰고 나머지를 계속 보여준다”가 더 유리할 수도 있습니다.
1) 사용자 탈퇴/비공개 전환 등으로 파트너 정보가 일시적으로 비어도 목록 전체를 차단할지 정책을 확인해 주세요.
2) 최신 메시지가 없는 방에서 content=""와 createdAt=null 폴백이 계약과 일치하는지 확인해 주세요. 직렬화/클라이언트 파싱이 null을 허용하는지 검토해 주세요.
3) findPartner가 본 메서드와 getChatRoomData 양쪽에서 두 번 호출됩니다. 1:1 방에서 비용은 작지만, 중복 계산을 줄이기 위해 roomId->partnerUserId 맵을 미리 만들어 전달하는 구조를 고려할 수 있습니다.

참고: 예외 대신 건너뛰기 방식을 택한다면, map 이전에 filter 단계에서 파트너 유효성 검사를 선행하는 패턴이 유용합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7060378 and 62abb86.

📒 Files selected for processing (6)
  • src/main/java/com/example/solidconnection/chat/dto/ChatRoomData.java (1 hunks)
  • src/main/java/com/example/solidconnection/chat/dto/UnreadCountDto.java (1 hunks)
  • src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java (2 hunks)
  • src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java (1 hunks)
  • src/main/java/com/example/solidconnection/chat/service/ChatService.java (2 hunks)
  • src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java (1)

4-5: 필요한 import 추가 적절.

    1. DTO 기반 투영(UnreadCountDto)과 일괄 조회(List) 사용이 PR 목표와 일치합니다.
src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java (1)

12-26: 아래 변경사항을 검토해주세요:

  1. 이식성 리스크 제거
     - JPQL의 ORDER BY … NULLS LAST는 Hibernate나 DB 벤더별로 지원 여부가 달라 파싱/실행 오류를 유발할 수 있습니다.
  2. 서비스 레이어 정렬 위임 (권장)
     - 최신 메시지 정렬 로직을 DB가 아닌 서비스 계층에서 처리해 NULLS LAST와 서브쿼리를 제거합니다.
  3. DB 정렬 유지 옵션 제시
     - LEFT JOIN + GROUP BY + MAX(m.createdAt)를 사용해 DB에서 정렬을 유지하는 대안을 제공합니다.
  4. 환경 설정 수동 검증 필요
     - 자동 스크립트 오류로 DB 프로바이더·JPA 환경을 직접 확인해 주세요.

변경 제안 A) 서비스 정렬 위임:

-    @Query("""
-           SELECT DISTINCT cr FROM ChatRoom cr
-           JOIN FETCH cr.chatParticipants
-           WHERE cr.id IN (
-               SELECT DISTINCT cp2.chatRoom.id
-               FROM ChatParticipant cp2
-               WHERE cp2.siteUserId = :userId
-           )
-           AND cr.isGroup = false
-           ORDER BY (
-               SELECT MAX(cm.createdAt)
-               FROM ChatMessage cm
-               WHERE cm.chatRoom = cr
-           ) DESC NULLS LAST
-           """)
+    @Query("""
+           SELECT DISTINCT cr FROM ChatRoom cr
+           JOIN cr.chatParticipants me
+           JOIN FETCH cr.chatParticipants
+           WHERE me.siteUserId = :userId
+             AND cr.isGroup = false
+           """)
     List<ChatRoom> findOneOnOneChatRoomsByUserIdWithParticipants(@Param("userId") long userId);

변경 제안 B) DB 정렬 유지:

SELECT DISTINCT cr
FROM ChatRoom cr
JOIN cr.chatParticipants me
LEFT JOIN cr.chatMessages m
WHERE me.siteUserId = :userId
  AND cr.isGroup = false
GROUP BY cr
ORDER BY MAX(m.createdAt) DESC
src/main/java/com/example/solidconnection/chat/service/ChatService.java (3)

24-27: import 추가 적절. 불필요 항목 없음.
새 DTO/집계 사용과 빈 리스트 처리에 필요한 import만 깔끔하게 추가되었습니다.


65-67: 빈 결과의 빠른 반환 처리 좋습니다.
불필요한 후속 배치 쿼리를 방지하고, 응답 시그니처도 명확해집니다.


63-63: 중복 반환 방지 로직 검증 완료

  1. 리포지토리 쿼리 중복 방지 확인
     - @Query에 이미 SELECT DISTINCT crJOIN FETCH cr.chatParticipants가 포함되어 있어, SQL 레벨에서 동일 ChatRoom의 중복 반환이 차단됩니다.
  2. 서비스 레이어 방어 코드 불필요
     - SQL 쿼리만으로 중복이 제거되므로, stream().distinct() 같은 추가 필터링은 필수 요건이 아닙니다.
  3. 선택적 방어 코드 안내
     - 만약 방어적 안전 장치를 더하고 싶다면, 아래 한 줄을 추가할 수 있습니다.
      java   chatRooms = chatRooms.stream().distinct().toList();   

이상으로 중복 관련 검증이 완료되어, 추가 수정은 필요치 않습니다.

Comment on lines +22 to +31
@Query("""
SELECT cm FROM ChatMessage cm
WHERE cm.id IN (
SELECT MAX(cm2.id)
FROM ChatMessage cm2
WHERE cm2.chatRoom.id IN :chatRoomIds
GROUP BY cm2.chatRoom.id
)
""")
List<ChatMessage> findLatestMessagesByChatRoomIds(@Param("chatRoomIds") List<Long> chatRoomIds);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

최신 메시지 선정 기준을 id → createdAt로 통일해 의미적 정합성 보장.

    1. 의미 불일치. 현재 쿼리는 MAX(id)로 "최신"을 결정하지만, ChatRoomRepository는 createdAt 기준으로 정렬합니다. 두 기준이 불일치하면 복원력 없는 결과가 됩니다(시퀀스 점프·데이터 마이그레이션·수동 삽입 시).
    1. 정확성. "최신"의 정의가 도메인적으로 생성시각(createdAt)이라면 쿼리도 동일 기준을 써야 합니다.
    1. 동률 처리. 동일 createdAt이 드물지만 가능하므로, 동률 시 더 큰 id를 고르는 보조 기준을 서비스/조합 단계에서 처리하는 것이 안전합니다(아래 ChatRoomData 개선안에 병행 제안).
    1. 빈 목록. IN (:chatRoomIds)로 빈 리스트가 들어오면 프로바이더에 따라 예외가 날 수 있어 호출부에서 빈 리스트 가드가 필요합니다.

ChatMessage의 @GeneratedValue 전략과 createdAt 세터·감사 필드 전략을 확인해 일관된 시간 정렬이 가능한지 점검해 주세요.

아래와 같이 쿼리를 createdAt 기준으로 바꾸는 것을 권장합니다.

-    @Query("""
-           SELECT cm FROM ChatMessage cm
-           WHERE cm.id IN (
-               SELECT MAX(cm2.id)
-               FROM ChatMessage cm2
-               WHERE cm2.chatRoom.id IN :chatRoomIds
-               GROUP BY cm2.chatRoom.id
-           )
-           """)
+    @Query("""
+           SELECT cm FROM ChatMessage cm
+           WHERE cm.chatRoom.id IN :chatRoomIds
+           AND cm.createdAt = (
+               SELECT MAX(cm2.createdAt)
+               FROM ChatMessage cm2
+               WHERE cm2.chatRoom = cm.chatRoom
+           )
+           """)
     List<ChatMessage> findLatestMessagesByChatRoomIds(@Param("chatRoomIds") List<Long> chatRoomIds);

🏁 Script executed:

#!/bin/bash
# ChatMessage ID/시간 필드 정의 확인
rg -n -C3 'class\s+ChatMessage\b'
rg -n -C2 '@GeneratedValue|createdAt' --type=java

Length of output: 37996


ChatMessageRepository의 최신 메시지 선별 기준을 createdAt으로 통일하기

  1. 쿼리 기준 통일

    • ChatRoomRepository는 메시지 최신 순 선별에 createdAt을 사용합니다.
    • 반면 ChatMessageRepository는 MAX(id)로 최신 메시지를 선택하고 있어 두 결과가 불일치할 수 있습니다.
    • 아래 코드처럼 쿼리를 createdAt 기준으로 수정해주세요.
    -@Query("""
    -       SELECT cm FROM ChatMessage cm
    -       WHERE cm.id IN (
    -           SELECT MAX(cm2.id)
    -           FROM ChatMessage cm2
    -           WHERE cm2.chatRoom.id IN :chatRoomIds
    -           GROUP BY cm2.chatRoom.id
    -       )
    -       """)
    +@Query("""
    +       SELECT cm FROM ChatMessage cm
    +       WHERE cm.chatRoom.id IN :chatRoomIds
    +       AND cm.createdAt = (
    +           SELECT MAX(cm2.createdAt)
    +           FROM ChatMessage cm2
    +           WHERE cm2.chatRoom = cm.chatRoom
    +       )
    +       """)
    List<ChatMessage> findLatestMessagesByChatRoomIds(@Param("chatRoomIds") List<Long> chatRoomIds);
  2. createdAt 필드 설정 확인

    • ChatMessage는 BaseEntity를 상속받아 @PrePersist에서 createdAt이 ZonedDateTime.now(UTC).truncatedTo(MICROS)로 자동 설정됩니다.
    • 따라서 createdAt을 비교 기준으로 사용하는 것이 도메인적으로도 일관되고 안전합니다.
  3. 빈 리스트 처리

    • IN절에 빈 컬렉션(chatRoomIds)이 들어갈 경우 일부 JPA 구현체에서 예외가 발생할 수 있습니다.
    • 호출부에서 빈 리스트인 경우 바로 빈 결과를 반환하거나, 레포지토리 호출 전 guard 로직을 추가해주세요.
  4. 동률 처리

    • 동일한 createdAt을 가지는 메시지가 극히 드물지만 가능하므로, 동률 시 더 큰 id를 선택하는 보조 로직을 서비스 레이어에서 추가 적용을 권장합니다.
  • 대상 파일: src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java (22–31번 라인)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Query("""
SELECT cm FROM ChatMessage cm
WHERE cm.id IN (
SELECT MAX(cm2.id)
FROM ChatMessage cm2
WHERE cm2.chatRoom.id IN :chatRoomIds
GROUP BY cm2.chatRoom.id
)
""")
List<ChatMessage> findLatestMessagesByChatRoomIds(@Param("chatRoomIds") List<Long> chatRoomIds);
@Query("""
SELECT cm FROM ChatMessage cm
WHERE cm.chatRoom.id IN :chatRoomIds
AND cm.createdAt = (
SELECT MAX(cm2.createdAt)
FROM ChatMessage cm2
WHERE cm2.chatRoom = cm.chatRoom
)
""")
List<ChatMessage> findLatestMessagesByChatRoomIds(@Param("chatRoomIds") List<Long> chatRoomIds);
🤖 Prompt for AI Agents
In
src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java
around lines 22-31, the repository currently selects latest messages by MAX(id)
which conflicts with ChatRoomRepository that uses createdAt; change the JPQL to
select messages whose createdAt equals the MAX(cm2.createdAt) grouped by
cm2.chatRoom.id (i.e. use MAX(cm2.createdAt) in the subquery and compare
cm.createdAt), keep the method signature and @Param unchanged, and ensure
callers guard against an empty chatRoomIds (return empty list before calling
repository) to avoid IN () issues; optionally handle tie-breakers (same
createdAt) in the service layer by selecting the message with the larger id when
createdAt values are equal.

Copy link
Member

@whqtker whqtker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정말 고생 많으셨습니다 !! 👍

@Gyuhyeok99 Gyuhyeok99 merged commit 1e5f2a4 into solid-connection:develop Aug 28, 2025
2 checks passed
@Gyuhyeok99 Gyuhyeok99 deleted the refactor/458-chatroom-n-plus-one-optimization branch September 25, 2025 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: 채팅방 관련 n + 1 문제 개선

2 participants